{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Properties and Descriptors"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start by creating a property using the decorator syntax:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from numbers import Integral\n",
    "\n",
    "class Person:\n",
    "    @property\n",
    "    def age(self):\n",
    "        return getattr(self, '_age', None)\n",
    "    \n",
    "    @age.setter\n",
    "    def age(self, value):\n",
    "        if not isinstance(value, Integral):\n",
    "            raise ValueError('age: must be an integer.')\n",
    "        if value < 0:\n",
    "            raise ValueError('age: must be a non-negative integer.')\n",
    "        self._age = value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "age: must be a non-negative integer.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.age = -10\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And notice how the instance dictionary does not contain `age`, even though we have that instance `age` attribute:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.age = 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(10, {'_age': 10})"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.age, p.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, let's rewrite this using a `property` class instead of the decorators:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def get_age(self):\n",
    "        return getattr(self, '_age', None)\n",
    "    \n",
    "    def set_age(self, value):\n",
    "        if not isinstance(value, Integral):\n",
    "            raise ValueError('age: must be an integer.')\n",
    "        if value < 0:\n",
    "            raise ValueError('age: must be a non-negative integer.')\n",
    "        self._age = value\n",
    "        \n",
    "    age = property(fget=get_age, fset=set_age)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And this works the exact same way as before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "age: must be a non-negative integer.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.age = -10\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.age = 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(10, {'_age': 10})"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.age, p.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, in both cases the property object instance can be accessed by using the class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "prop = Person.age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<property at 0x7f9f503d1db8>"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "prop"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And this property, is actually a data descriptor!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(prop, '__set__')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(prop, '__get__')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case, our property has both the `__get__` and `__set__` methods so we ended up with a data descriptor."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Even if we only defined a read-only property, we would still end up with a data descriptor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime\n",
    "\n",
    "class TimeUTC:\n",
    "    @property\n",
    "    def current_time(self):\n",
    "        return datetime.utcnow().isoformat()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'2019-07-13T20:48:18.993428'"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = TimeUTC()\n",
    "t.current_time"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "prop = TimeUTC.current_time"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(prop, '__get__')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(prop, '__set__')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But the internal implemetation of the `__set__` method would refuse to set a value:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "can't set attribute\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    t.current_time = datetime.utcnow().isoformat()\n",
    "except AttributeError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, if properties are implemented using data descriptors - this means that instance attributes with the same name will not shadow the descriptor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{}"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "t.__dict__['current_time'] = 'not a time'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'current_time': 'not a time'}"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'2019-07-13T20:48:19.099088'"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t.current_time"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so given what we know about data descriptors all this should make sense."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's try to implement our own version of the property type, decorators and all!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MakeProperty:\n",
    "    def __init__(self, fget=None, fset=None):\n",
    "        self.fget = fget\n",
    "        self.fset = fset\n",
    "        \n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        print('__get__ called...')\n",
    "        if instance is None:\n",
    "            return self\n",
    "        if self.fget is None:\n",
    "            raise AttributeError(f'{self.prop_name} is not readable.')\n",
    "        return self.fget(instance)\n",
    "            \n",
    "    def __set__(self, instance, value):\n",
    "        print('__set__ called...')\n",
    "        if self.fset is None:\n",
    "            raise AttributeError(f'{self.prop_name} is not writable.')\n",
    "        self.fset(instance, value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is now sufficient to start creating properties using this data descriptor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def get_name(self):\n",
    "        return self._name\n",
    "    \n",
    "    def set_name(self, value):\n",
    "        self._name = value\n",
    "        \n",
    "    name = MakeProperty(fget=get_name, fset=set_name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{}"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__set__ called...\n"
     ]
    }
   ],
   "source": [
    "p.name = 'Guido'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Guido'"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And even if we try to shadow the property name in the instance, things will work just fine:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.__dict__['name'] = 'Alex'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'_name': 'Guido', 'name': 'Alex'}"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Guido'"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we would like to have a decorator approach as well. To do that we're going to mimic the way the property decorators work (you may want to go back to those lectures and refresh your memory if needed)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So how should the `@MakeProperty` decorator work?\n",
    "\n",
    "It should take a function and return a descriptor object. \n",
    "\n",
    "In turn, that descriptor object should have a `setter` method that we can call to *add* the setter method to the descriptor, that also returns the descriptor object - just like we have with `property` types:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MakeProperty:\n",
    "    def __init__(self, fget=None, fset=None):\n",
    "        self.fget = fget\n",
    "        self.fset = fset\n",
    "        \n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        print('__get__ called...')\n",
    "        if instance is None:\n",
    "            return self\n",
    "        if self.fget is None:\n",
    "            raise AttributeError(f'{self.prop_name} is not readable.')\n",
    "        return self.fget(instance)\n",
    "            \n",
    "    def __set__(self, instance, value):\n",
    "        print('__set__ called...')\n",
    "        if self.fset is None:\n",
    "            raise AttributeError(f'{self.prop_name} is not writable.')\n",
    "        self.fset(instance, value)\n",
    "        \n",
    "    def setter(self, fset):\n",
    "        self.fset = fset\n",
    "        return self\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So both the `__init__` and the `setter` methods can be used like decorators, and we can now use our `MakeProperty` class with decorator syntax:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can do it the \"long\" way first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def get_first_name(self):\n",
    "        return getattr(self, '_first_name', None)\n",
    "    \n",
    "    def set_first_name(self, value):\n",
    "        self._first_name = value\n",
    "        \n",
    "    def get_last_name(self):\n",
    "        return getattr(self, '_last_name', None)\n",
    "    \n",
    "    def set_last_name(self, value):\n",
    "        self._last_name = value\n",
    "        \n",
    "    first_name = MakeProperty(fget=get_first_name, fset=set_first_name)\n",
    "    last_name = MakeProperty(fget=get_last_name, fset=set_last_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or, we can use the \"shorthand\" decorator syntax:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @MakeProperty\n",
    "    def first_name(self):\n",
    "        return getattr(self, '_first_name', None)\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self._first_name = value\n",
    "        \n",
    "    @MakeProperty\n",
    "    def last_name(self):\n",
    "        return getattr(self, '_last_name', None)\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self._last_name = value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "p1 = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__set__ called...\n"
     ]
    }
   ],
   "source": [
    "p1.first_name = 'Raymond'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__set__ called...\n"
     ]
    }
   ],
   "source": [
    "p1.last_name = 'Hettinger'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Raymond'"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p1.first_name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Hettinger'"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p1.last_name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And of course this will work with multiple instances of the `Person` class since we are using the instances themselves for the underlying storage:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__set__ called...\n",
      "__set__ called...\n"
     ]
    }
   ],
   "source": [
    "p2 = Person()\n",
    "p2.first_name, p2.last_name = 'Alex', 'Martelli'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called...\n",
      "__get__ called...\n",
      "__get__ called...\n",
      "__get__ called...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "('Raymond', 'Hettinger', 'Alex', 'Martelli')"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p1.first_name, p1.last_name, p2.first_name, p2.last_name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course our implementation is quite simplistic, but it should help solidy our understanding of properties, descriptors, and decorators too!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
